package dynsim.graphics.plot.j3d.axis;

public class AxisLabelCalculator {

	/* VARIABLES */
	private double data_min = 0d, data_max = 1d; // actual min/max for the
	// data set

	private double plot_min = 0d, plot_max = 1d; // min/max on the axis
	// itself

	private AxisLabel[] labels;

	private int nDivisions = 0;

	private boolean labelsValid;

	public void createNewLabels(double min, double max) {
		data_min = min;
		data_max = max;
		int minNumberOfDivisions = 1;
		int maxNumberOfDivisions = 10;
		double log10 = Math.log(10.0);
		int maxCharsPerLabel = 5;
		labelsValid = true;

		// log_max is the logarithm of the max value of the data
		double log_max = data_max == 0d ? 0d : Math.log(Math.abs(data_max)) / log10;

		// int_log_max is the floored integer equivalent of log_max
		final int int_log_max = (int) Math.floor(log_max);

		// by default, scale_power is 0
		// This number represents the amount by which we scale all labels. For
		// example,
		// if our range is from 2,000,000,000 to 5,000,000,000 we want
		// scale_power to be
		// 9 so that we get 2.0, 2.5, 3.0, ... or something like that on the
		// labels.
		int scale_power = 0;
		if (int_log_max >= maxCharsPerLabel)
			// we have an order of magnitude that can't be displayed in standard
			// form, so we need to set
			// a value for scale_power
			scale_power = int_log_max;
		else if (int_log_max <= -maxCharsPerLabel)
			// we have an order of magnitude that can't be displayed in standard
			// form, so we need to set
			// a value for scale_power
			scale_power = int_log_max;

		// System.out.println();
		// System.out.println("int_log_max = " + int_log_max + ",
		// maxCharsPerLabel = " + maxCharsPerLabel + ", scale_power = " +
		// scale_power);

		final DoubleNumberFormatter format = new DoubleNumberFormatter(scale_power);
		// the formatter uses scale_power when creating labels

		/*
		 * Our strategy here is based on the observation that plotting the range
		 * 0.2 to 40 is very similar to the task of plotting the ranges 2 to
		 * 400, or 0.02 to 4. We scale the min and max by an order of ten such
		 * that when converted to integers (by a process not quite like
		 * trucation) the difference between those two integers is a number
		 * greater than or equal to 10 and less than (but not equal to) 100. In
		 * other words, there will be a difference of exactly two digits, which
		 * is a sensible precision to see in variation between axis labels. For
		 * example, the ranges listed above would all yield the integer pair 1
		 * to 40 (because the difference between those integers has two digits).
		 * Given a pair of integers, the function proceeds to calculate
		 * appropriate labels. If we are using the suggested range, we just
		 * round the min down to the next nice label and we round the max up to
		 * the next nice label (unless the max and min are already on nice
		 * labels).
		 */

		final double difference = data_max - data_min;
		final double pow = Math.floor(Math.log(difference) / log10) - 1.0;
		// pow is the power of 10 used to get integers of the appropriate range

		int fractDigits = 0;
		if (scale_power > 0)
			fractDigits = scale_power - (int) pow;
		else if (pow < -0.5)
			// we use -0.5 instead of 0.0 in case a Math.floor() returns
			// something that should be 0.0
			// but is really just barely under 0.0
			fractDigits = scale_power - (int) pow;

		final double conversion = Math.pow(10.0, pow);
		// this is the actual conversion factor we used, stored once to keep
		// from
		// having to recalculate it

		int intMin = round(data_min / conversion, false);
		int intMax = round(data_max / conversion, true);
		// we now have intMin and intMax: the integer pair with a two-digit
		// difference

		plot_min = data_min;
		plot_max = data_max;

		final int naturalNumberOfDivisions = intMax - intMin;
		// this number has precisely two digits

		int nDivisions = 0;

		final float idealMinFraction = 0.5f;
		// we will allow as few as this fraction of the maximum number of labels
		// if it is convenient

		int nUnits = 1;
		// this number can represent two things:
		// a) if naturalNumberOfDivisions < maxNumberOfDivisions
		// it represents the number of divisions (units) between the natural
		// divisions
		// b) if naturalNumberOfDivisions > maxNumberOfDivisions
		// it represents the number of units between divisions (the number to
		// skip between divisions)
		// 1 is the default value, but we will see if a different value might be
		// better

		if (naturalNumberOfDivisions < maxNumberOfDivisions)
		// we might like to put in some new divisions
		{
			final float proximity = (float) naturalNumberOfDivisions / (float) maxNumberOfDivisions;
			// this number measures how close the natural number is to the
			// maximum

			boolean niceDivisionFound = false;
			if (proximity < idealMinFraction)
			// we want to do something because the number we have is below the
			// range we want
			{
				final int[] divisions = { 2, 4, 5, 10, 20 };
				// These are the numbers of subdivisions we might want to place
				// between natural divisions.
				// The array only goes up to 20 because we would need a plot
				// with at least 200 labels before
				// needing to go any higher.
				for (int i = 0; i < divisions.length; i++) {
					final int candidate = divisions[i];
					if (proximity * candidate <= 1.0)
					// this might work, because the number is small enough that
					// we could fit
					// this many divisions on the axis
					{
						niceDivisionFound = true;
						nUnits = candidate;

						// the next iteration might be even better, so we...
						continue;
					}

					// if we didn't execute continue, it was because we can't
					// fit this many
					// divisions on the axis, and so there's no point in trying
					// the next
					// iteration either because it's even bigger, so we break
					// out of the loop
					break;
				}
			}
			if (niceDivisionFound) {
				nDivisions = naturalNumberOfDivisions * nUnits + (int) ((plot_max / conversion - intMax) * nUnits);
				/*
				 * The first term in the expression isn't tough: If nUnits is 2
				 * then we need twice as many divisions on the axis. The second
				 * term isn't so obvious. Suppose our axis goes from 0 to 12.7
				 * and we decide to set nUnits to 2. We therefore get labels
				 * 0.0, 0.5, 1.0, ... , 12.0 but we won't get 12.5 on there.
				 * There will instead be empty space where the 12.5 should go.
				 * The last term accounts for this, by taking the difference
				 * between the top label and the axis max (in this case 12.7 -
				 * 12.0 = 0.7), multiplying bn nUnits (to get 1.4 in this case)
				 * and truncating (to get 1 in this case). The result (1 in this
				 * case) is the number of labels extra we need.
				 */

				if (pow < 0.5 || scale_power > 0)
					// we use 0.5 instead of 1.0 in case a Math.floor() returns
					// something
					// that should be 1.0 but is really just barely under 1.0
					fractDigits++;
				// we've gone down to one lover decimal level so we have to tell
				// the formatter

				if ((nUnits == 4 || nUnits == 20) && (pow < 1.5 || scale_power > 0)) // we
					// 've
					// actually
					// gone
					// down
					// two
					// decimal
					// levels,
					// so...
					// we use 1.5 instead of 2.0 in case a Math.floor() returns
					// something
					// that should be 2.0 but is really just barely under 2.0
					fractDigits++; // we add another
			} else
			// we give up and keep the natural number, even though it's smaller
			// that what we'd like
			{
				nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);
			}
		} else if (naturalNumberOfDivisions > maxNumberOfDivisions)
		// the natural number is larger than what we'd like, so we have to skip
		// over some
		// (typically this is the more common problem)
		{
			nDivisions = 1;
			// we supply this initialization to make the compiler happy, but in
			// the algorithm
			// requires that this initial value change

			final int[] skips = { 2, 5, 10, 20, 25, 50 };
			// These are the numbers of natural divisions we're going to try
			// skipping.

			for (int i = 0; i < skips.length; i++) {
				final int nDivisionsThisTry = naturalNumberOfDivisions / skips[i];
				if (nDivisionsThisTry > maxNumberOfDivisions) {
					// this many skips isn't big enough, so we'll try the next
					// one
					continue;
				}

				// we now assign to nUnits the number of natural divisions to
				// skip over
				nUnits = skips[i];
				nDivisions = nDivisionsThisTry;

				if (nUnits >= 10 && nUnits != 25 && fractDigits > 0)
					// we're skipping at least an order of 10, and we're not in
					// quarters, so...
					fractDigits--; // we can get rid of one fraction digit

				/*
				 * We may calculate a new value for the intMin. Consider, for
				 * example, the range 3 to 17. If we decide that our skip will
				 * be 2, we will get labels like 3, 5, 7, 9, etc. This will look
				 * dumb because we would much rather have the first label a nice
				 * multiple of our skip (i.e., we would rather have 2, 4, 6, 8,
				 * etc., or for multiples of 5 we would rather have 20, 25, 30,
				 * 35, etc. over 18, 23, 28, 33, etc.) Therefore, if the intMin
				 * is not a nice multiple of that skip then we increase the
				 * intMin, and we may have to decrement nDivisions because of a
				 * lost label.
				 */
				if (intMin % nUnits != 0)
				// only true if we are not using the suggested range
				{
					// increase is the amount we will increase intMin by to make
					// it a nice multiple of nUnits
					final int increase = intMin > 0 ? nUnits - intMin % nUnits : -intMin % nUnits;

					if (increase > intMax - (intMax - intMin) / nUnits * nUnits - intMin)
						// we have put the last label over the limit, so...
						nDivisions--; // we drop the last label

					intMin += increase;
				}

				// We are happy with what we've got because it gives us an
				// acceptable
				// number of divisions. We don't want to go any higher because
				// that
				// will just make for fewer divisions, so we...
				break;
			}
		} else
			// hey! they're exactly equal
			nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);

		double minLabelValue = intMin * conversion;
		final double inc = naturalNumberOfDivisions < maxNumberOfDivisions ? conversion / nUnits : conversion * nUnits;
		if (naturalNumberOfDivisions < maxNumberOfDivisions && minLabelValue - inc >= plot_min)
		// this happens if we are dividing up divisions, and we get divisions
		// below intMin * conversion
		{
			int nLost = (int) ((minLabelValue - inc) / inc);
			minLabelValue -= nLost * inc;
			nDivisions += nLost;
		}
		labels = new AxisLabel[nDivisions + 1];
		this.nDivisions = nDivisions;

		// System.out.println("fractDigits = " + fractDigits + ", minLabelValue
		// = " + minLabelValue + ", inc = " + inc);

		format.setFractionDigits(fractDigits);
		for (int j = 0; j < labels.length; j++) {
			final double labelValue = minLabelValue + j * inc;

			labels[j] = new AxisLabel();
			labels[j].text = format.format(labelValue);
			labels[j].position = (labelValue - plot_min) / (plot_max - plot_min);
			// labels[j].logPosition = Math.log(labels[j].position + Math.E);
			if (labels[j].position < 0.)
				labels[j].position = 0.;
		}
	}

	// private int charsReq(int pow)
	// /*
	// * Returns the number of characters required (not using scientific
	// * notation) to represent a decade of the given order of magnitude.
	// */
	// {
	// if (pow < 0)
	// /*
	// * If the power is less than 0, we need one space for the
	// * leading zero, one for the decimal, and then -pow spaces
	// * for each of the following characters. For example, if
	// * pow = -2 then the result would be 4, because there are
	// * four characters in the string "0.01".
	// */
	// return -pow + 2;
	// else
	// /*
	// * If the power is 0 or greater, we need one character for
	// * the character '1' and one '0' for each order of magnitude.
	// */
	// return pow + 1;
	// }

	private int round(final double d, final boolean down)
	/*
	 * Determines an integer value from a double by rounding intelligently. In
	 * the logarithmic case, when determining the order of magnitude of the
	 * lowest tick, the "down" parameter is false because we want to round up
	 * from the minimum point in the axis (so that the tick shows up on the
	 * range of the axis) if we are not very close to an integer value. However,
	 * when we are determining the order of magnitude of the highest tick, we
	 * want to round down if we are not very close to an integer so that the
	 * tick appears within the range of the axis. Similarly in the case for the
	 * linear axis, we round up to get the smallest tick value and we round down
	 * to get the largest tick value. We do exactly the opposite if wew are
	 * using the suggested range. In that case, we round down to get the minimum
	 * and up to get the maximum.
	 */
	{
		final double minProximity = 0.0001;
		/*
		 * A parameter's proximity to the nearest integer must be this fraction
		 * of its size in order to be considered that value.
		 */
		final double round = Math.round(d); // the closest integer value
		if (d == round || Math.abs(d - round) < (d != 0.0 ? minProximity * Math.abs(d) : 0.000001)) {
			// we assume here that d is close enough to be an integer, so we
			// round and return
			return (int) round;
		} else {
			// d is not close enough to be an integer, so we return the
			// floor if we were supposed to round down and ceil
			// otherwise
			return down ? (int) Math.floor(d) : (int) Math.ceil(d);
		}
	}

	public String[] getLabels() {
		int len = labels.length;
		String[] lab = new String[len];
		for (int i = 0; i < len; ++i)
			lab[i] = labels[i].text;
		return lab;
	}

	public double[] getPositions() {
		int len = labels.length;
		double[] pos = new double[len];
		for (int i = 0; i < len; ++i)
			pos[i] = labels[i].position;
		return pos;
	}

	public void printLabels() {
		System.out.println("data_min = " + data_min + ", data_max = " + data_max);
		System.out.println("plot_min = " + plot_min + ", plot_max = " + plot_max);
		System.out.println("nDivisions = " + nDivisions + ", labelsValid = " + labelsValid);

		int i;

		for (i = 0; i < labels.length; ++i) {
			System.out.println("label " + (i + 1) + ": " + labels[i].text + "  position: " + labels[i].position);
		}
	}

	// This class is used when storing information about
	// the labels on the axis.
	private class AxisLabel {
		String text;

		double position;
		// double logPosition;
	}

	/**
	 * Just for testing
	 */
	public static void main(String[] argv) {
		AxisLabelCalculator t = new AxisLabelCalculator();
		t.createNewLabels(0., 1.);
		t.printLabels();
		t.createNewLabels(0., 100.);
		t.printLabels();
		t.createNewLabels(0.0001, 0.001);
		t.printLabels();
		t.createNewLabels(5., 10.);
		t.printLabels();
		t.createNewLabels(.001, .050);
		t.printLabels();
		t.createNewLabels(.00001, .00005);
		t.printLabels();
		t.createNewLabels(-.001, .00005);
		t.printLabels();
		t.createNewLabels(-.001, .005);
		t.printLabels();
		t.createNewLabels(-2., 5.);
		t.printLabels();
		t.createNewLabels(.05, 10.);
		t.printLabels();

	}

}
